(*******************************************
*                                          *
* Graphic Scene Engine, http://glscene.org *
*                                          *
********************************************)

unit GXS.PerlinPFX;

(* PFX particle effects revolving around the use of Perlin noise *)

interface

{$I Scene.inc}

uses
  System.Classes,
  System.Math,

  Scene.Import.OpenGLx,
  Scene.VectorGeometry,
  GXS.ParticleFX,
  GXS.Graphics;
  
const
  cPERLIN_TABLE_SIZE = 256; // must be a power of two

type

   (* A sprite-based particles FX manager using perlin-based sprites.
      This PFX manager is more suited for smoke or fire effects, and with proper
      tweaking of the texture and perlin parameters, may help render a convincing
      effect with less particles.
      The sprite generate by this manager is the composition of a distance-based
      intensity and a perlin noise. *)
   TgxPerlinPFXManager = class(TgxBaseSpritePFXManager)
   private
     FTexMapSize: Integer;
     FNoiseSeed: Integer;
     FNoiseScale: Integer;
     FNoiseAmplitude: Integer;
     FSmoothness: Single;
     FBrightness, FGamma: Single;
   protected
     procedure PrepareImage(bmp32: TgxBitmap32; var texFormat: Integer); override;
     procedure SetTexMapSize(const val: Integer);
     procedure SetNoiseSeed(const val: Integer);
     procedure SetNoiseScale(const val: Integer);
     procedure SetNoiseAmplitude(const val: Integer);
     procedure SetSmoothness(const val: Single);
     procedure SetBrightness(const val: Single);
     procedure SetGamma(const val: Single);
   public
     constructor Create(aOwner: TComponent); override;
     destructor Destroy; override;
   published
     (* Underlying texture map size, as a power of two.
       Min value is 3 (size=8), max value is 9 (size=512). *)
     property TexMapSize: Integer read FTexMapSize write SetTexMapSize default 6;
     (* Smoothness of the distance-based intensity.
       This value is the exponent applied to the intensity in the texture,
       basically with a value of 1 (default) the intensity decreases linearly,
       with higher values, it will remain 'constant' in the center then
       fade-off more abruptly, and with values below 1, there will be a
       sharp spike in the center. *)
     property Smoothness: Single read FSmoothness write SetSmoothness;
     (* Brightness factor applied to the perlin texture intensity.
       Brightness acts as a scaling, non-saturating factor. Examples:
       Brightness = 1 : intensities in the [0; 1] range
       Brightness = 2 : intensities in the [0.5; 1] range
       Brightness = 0.5 : intensities in the [0; 0.5] range
       Brightness is applied to the final texture (and thus affects
       the distance based intensity). *)
     property Brightness: Single read FBrightness write SetBrightness;
     property Gamma: Single read FGamma write SetGamma;
     // Random seed to use for the perlin noise.
     property NoiseSeed: Integer read FNoiseSeed write SetNoiseSeed default 0;
     // Scale applied to the perlin noise (stretching).
     property NoiseScale: Integer read FNoiseScale write SetNoiseScale default 100;
     (* Amplitude applied to the perlin noise (intensity).
       This value represent the percentage of the sprite luminance affected by
       the perlin texture. *)
     property NoiseAmplitude: Integer read FNoiseAmplitude write SetNoiseAmplitude default 50;
     property ColorMode default scmInner;
     property SpritesPerTexture default sptFour;
     property ParticleSize;
     property ColorInner;
     property ColorOuter;
     property LifeColors;
   end;

  (* Generates Perlin Noise in the [-1; 1] range.
    2D noise requests are taken in the Z=0 slice *)
  TgxPerlin3DNoise = class(TObject)
  protected
    FPermutations: packed array [0 .. cPERLIN_TABLE_SIZE - 1] of Integer;
    FGradients: packed array [0 .. cPERLIN_TABLE_SIZE * 3 - 1] of Single;
  protected
    function Lattice(ix, iy, iz: Integer; fx, fy, fz: Single): Single; overload;
    function Lattice(ix, iy: Integer; fx, fy: Single): Single; overload;
  public
    constructor Create(randomSeed: Integer);
    procedure Initialize(randomSeed: Integer);
    function Noise(const x, y: Single): Single; overload;
    function Noise(const x, y, z: Single): Single; overload;
    function Noise(const v: TAffineVector): Single; overload;
    function Noise(const v: TVector): Single; overload;
  end;


// ------------------------------------------------------------------
implementation
// ------------------------------------------------------------------

// ------------------
// ------------------ TgxPerlinPFXManager ------------------
// ------------------

constructor TgxPerlinPFXManager.Create(aOwner : TComponent);
begin
   inherited;
   FTexMapSize:=6;
   FNoiseScale:=100;
   FNoiseAmplitude:=50;
   FSmoothness:=1;
   FBrightness:=1;
   FGamma:=1;
   SpritesPerTexture:=sptFour;
   ColorMode:=scmInner;
end;

destructor TgxPerlinPFXManager.Destroy;
begin
   inherited Destroy;
end;

procedure TgxPerlinPFXManager.SetTexMapSize(const val : Integer);
begin
   if val<>FTexMapSize then 
   begin
      FTexMapSize:=val;
      if FTexMapSize<3 then 
	    FTexMapSize:=3;
      if FTexMapSize>9 then 
	    FTexMapSize:=9;
      NotifyChange(Self);
   end;
end;

procedure TgxPerlinPFXManager.SetNoiseSeed(const val : Integer);
begin
   if val<>FNoiseSeed then 
   begin
      FNoiseSeed:=val;
      NotifyChange(Self);
   end;
end;

procedure TgxPerlinPFXManager.SetNoiseScale(const val : Integer);
begin
   if val<>FNoiseScale then 
   begin
      FNoiseScale:=val;
      NotifyChange(Self);
   end;
end;

procedure TgxPerlinPFXManager.SetNoiseAmplitude(const val : Integer);
begin
   if val<>FNoiseAmplitude then 
   begin
      FNoiseAmplitude:=val;
      if FNoiseAmplitude<0 then 
	    FNoiseAmplitude:=0;
      if FNoiseAmplitude>100 then 
	    FNoiseAmplitude:=100;
      NotifyChange(Self);
   end;
end;

procedure TgxPerlinPFXManager.SetSmoothness(const val : Single);
begin
   if FSmoothness<>val then 
   begin
      FSmoothness:=ClampValue(val, 1e-3, 1e3);
      NotifyChange(Self);
   end;
end;

procedure TgxPerlinPFXManager.SetBrightness(const val : Single);
begin
   if FBrightness<>val then 
   begin
      FBrightness:=ClampValue(val, 1e-3, 1e3);
      NotifyChange(Self);
   end;
end;

procedure TgxPerlinPFXManager.SetGamma(const val : Single);
begin
   if FGamma<>val then 
   begin
      FGamma:=ClampValue(val, 0.1, 10);
      NotifyChange(Self);
   end;
end;

procedure TgxPerlinPFXManager.PrepareImage(bmp32 : TgxBitmap32; var texFormat : Integer);

   procedure PrepareSubImage(dx, dy, s : Integer; noise : TgxPerlin3DNoise);
   var
      s2 : Integer;
      x, y, d : Integer;
      is2, f, fy, pf, nBase, nAmp, df, dfg : Single;
      invGamma : Single;
      scanLine : PPixel32Array;
      gotIntensityCorrection : Boolean;
   begin
      s2:=s shr 1;
      is2:=1/s2;
      pf:=FNoiseScale*0.05*is2;
      nAmp:=FNoiseAmplitude*(0.01);
      nBase:=1-nAmp*0.5;

      if Gamma<0.1 then
         invGamma:=10
      else 
	    invGamma:=1/Gamma;
      gotIntensityCorrection:=(Gamma<>1) or (Brightness<>1);

      for y:=0 to s-1 do 
	  begin
         fy:=Sqr((y+0.5-s2)*is2);
         scanLine := bmp32.ScanLine[y+dy];
         for x:=0 to s-1 do 
		 begin
            f:=Sqr((x+0.5-s2)*is2)+fy;
            if f<1 then 
			begin
               df := nBase+nAmp * noise.Noise(x*pf, y*pf);
               if gotIntensityCorrection then
                  df := ClampValue(Power(df, InvGamma)*Brightness, 0, 1);
               dfg := Power((1-Sqrt(f)), FSmoothness);
               d := Trunc(df*255);
               if d > 255 then 
			     d:=255;
               with scanLine^[x+dx] do 
			   begin
                  r:=d;
                  g:=d;
                  b:=d;
                  a:=Trunc(dfg*255);
               end;
            end 
			else 
			  PInteger(@scanLine[x+dx])^:=0;
         end;
      end;
   end;

var
   s, s2 : Integer;
   noise : TgxPerlin3DNoise;
begin
   s:=(1 shl TexMapSize);
   bmp32.Width:=s;
   bmp32.Height:=s;
   bmp32.Blank := false;
   texFormat:=GL_LUMINANCE_ALPHA;
   noise:=TgxPerlin3DNoise.Create(NoiseSeed);
   try
      case SpritesPerTexture of
         sptOne: PrepareSubImage(0, 0, s, noise);
         sptFour: 
		 begin
            s2:=s div 2;
            PrepareSubImage(0, 0, s2, noise);
            noise.Initialize(NoiseSeed+1);
            PrepareSubImage(s2, 0, s2, noise);
            noise.Initialize(NoiseSeed+2);
            PrepareSubImage(0, s2, s2, noise);
            noise.Initialize(NoiseSeed+3);
            PrepareSubImage(s2, s2, s2, noise);
         end;
      else
         Assert(False);
      end;
   finally
      noise.Free;
   end;
end;

// ------------------
// ------------------ TgxPerlin3DNoise ------------------
// ------------------
constructor TgxPerlin3DNoise.Create(randomSeed: Integer);
begin
  inherited Create;
  Initialize(randomSeed);
end;

procedure TgxPerlin3DNoise.Initialize(randomSeed: Integer);
var
  seedBackup: Integer;
  i, t, j: Integer;
  z, r: Single;
begin
  seedBackup := RandSeed;
  RandSeed := randomSeed;

  // Generate random gradient vectors.
  for i := 0 to cPERLIN_TABLE_SIZE - 1 do
  begin
    z := 1 - 2 * Random;
    r := Sqrt(1 - z * z);
    SinCosine(c2PI * Random, r, FGradients[i * 3], FGradients[i * 3 + 1]);
    FGradients[i * 3 + 2] := z;
  end;
  // Initialize permutations table
  for i := 0 to cPERLIN_TABLE_SIZE - 1 do
    FPermutations[i] := i;
  // Shake up
  for i := 0 to cPERLIN_TABLE_SIZE - 1 do
  begin
    j := Random(cPERLIN_TABLE_SIZE);
    t := FPermutations[i];
    FPermutations[i] := FPermutations[j];
    FPermutations[j] := t;
  end;

  RandSeed := seedBackup;
end;

function TgxPerlin3DNoise.Lattice(ix, iy, iz: Integer;
  fx, fy, fz: Single): Single;
const
  cMask = cPERLIN_TABLE_SIZE - 1;
var
  g: Integer;
begin
  g := FPermutations[(ix + FPermutations[(iy + FPermutations[iz and cMask]) and
    cMask]) and cMask] * 3;
  Result := FGradients[g] * fx + FGradients[g + 1] * fy + FGradients
    [g + 2] * fz;
end;

function TgxPerlin3DNoise.Lattice(ix, iy: Integer; fx, fy: Single): Single;
const
  cMask = cPERLIN_TABLE_SIZE - 1;
var
  g: Integer;
begin
  g := FPermutations[(ix + FPermutations[(iy + FPermutations[0]) and cMask])
    and cMask] * 3;
  Result := FGradients[g] * fx + FGradients[g + 1] * fy;
end;

function TgxPerlin3DNoise.Noise(const v: TAffineVector): Single;

  function Smooth(var x: Single): Single;
  begin
    Result := x * x * (3 - 2 * x);
  end;

var
  ix, iy, iz: Integer;
  fx0, fx1, fy0, fy1, fz0, fz1: Single;
  wx, wy, wz: Single;
  vy0, vy1, vz0, vz1: Single;
begin
  ix := Floor(v.x);
  fx0 := v.x - ix;
  fx1 := fx0 - 1;
  wx := Smooth(fx0);

  iy := Floor(v.y);
  fy0 := v.y - iy;
  fy1 := fy0 - 1;
  wy := Smooth(fy0);

  iz := Floor(v.z);
  fz0 := v.z - iz;
  fz1 := fz0 - 1;
  wz := Smooth(fz0);

  vy0 := Lerp(Lattice(ix, iy, iz, fx0, fy0, fz0), Lattice(ix + 1, iy, iz, fx1,
    fy0, fz0), wx);
  vy1 := Lerp(Lattice(ix, iy + 1, iz, fx0, fy1, fz0),
    Lattice(ix + 1, iy + 1, iz, fx1, fy1, fz0), wx);
  vz0 := Lerp(vy0, vy1, wy);

  vy0 := Lerp(Lattice(ix, iy, iz + 1, fx0, fy0, fz1),
    Lattice(ix + 1, iy, iz + 1, fx1, fy0, fz1), wx);
  vy1 := Lerp(Lattice(ix, iy + 1, iz + 1, fx0, fy1, fz1),
    Lattice(ix + 1, iy + 1, iz + 1, fx1, fy1, fz1), wx);
  vz1 := Lerp(vy0, vy1, wy);

  Result := Lerp(vz0, vz1, wz);
end;

function TgxPerlin3DNoise.Noise(const x, y: Single): Single;

  function Smooth(var x: Single): Single;
  begin
    Result := x * x * (3 - 2 * x);
  end;

var
  ix, iy: Integer;
  fx0, fx1, fy0, fy1: Single;
  wx, wy: Single;
  vy0, vy1: Single;
begin
  ix := Floor(x);
  fx0 := x - ix;
  fx1 := fx0 - 1;
  wx := Smooth(fx0);

  iy := Floor(y);
  fy0 := y - iy;
  fy1 := fy0 - 1;
  wy := Smooth(fy0);

  vy0 := Lerp(Lattice(ix, iy, fx0, fy0), Lattice(ix + 1, iy, fx1, fy0), wx);
  vy1 := Lerp(Lattice(ix, iy + 1, fx0, fy1), Lattice(ix + 1, iy + 1, fx1,
    fy1), wx);
  Result := Lerp(vy0, vy1, wy);
end;

function TgxPerlin3DNoise.Noise(const x, y, z: Single): Single;
begin
  Result := Noise(AffineVectorMake(x, y, z));
end;

function TgxPerlin3DNoise.Noise(const v: TVector): Single;
begin
  Result := Noise(PAffineVector(@v)^);
end;


// ------------------------------------------------------------------
initialization
// ------------------------------------------------------------------

   RegisterClasses([TgxPerlinPFXManager]);

end.
